package org.example.dao;

import com.mxgraph.layout.mxIGraphLayout;
import com.mxgraph.layout.mxOrganicLayout;
import com.mxgraph.util.mxCellRenderer;
import com.mxgraph.util.mxConstants;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.*;
import javax.imageio.ImageIO;
import org.example.utils.OsUtils;
import org.jgrapht.Graph;
import org.jgrapht.GraphPath;
import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
import org.jgrapht.ext.JGraphXAdapter;
import org.jgrapht.graph.DefaultDirectedWeightedGraph;
import org.jgrapht.graph.DefaultWeightedEdge;

/**
 * WordGraph class represents a directed graph constructed from text.
 */
public class WordGraph {

  public final Graph<String, DefaultWeightedEdge> graph;
  private final Map<DefaultWeightedEdge, String> edgeColors;
  private final String defaultColor;
  private final OsUtils osUtils = new OsUtils();

  /**
   * No-argument constructor.
   */

  public WordGraph() {
    graph = new DefaultDirectedWeightedGraph<>(DefaultWeightedEdge.class);
    edgeColors = new HashMap<>();
    defaultColor = "#000000";
  }

  /**
   * Constructor that builds a directed graph from the given text.
   *
   * @param text the text to construct the graph from.
   */
  public WordGraph(String text) {
    this();
    buildGraphFromText(text);
  }

  private void buildGraphFromText(String text) {
    String[] words = text.split("\\s+");
    Map<String, Integer> edgeWeights = new HashMap<>();

    for (int i = 0; i < words.length - 1; i++) {
      String source = words[i].toLowerCase();
      String target = words[i + 1].toLowerCase();
      String edgeKey = source + "," + target;

      if (!graph.containsVertex(source)) {
        graph.addVertex(source);
      }
      if (!graph.containsVertex(target)) {
        graph.addVertex(target);
      }
      if (graph.containsEdge(source, target)) {
        int weight = edgeWeights.getOrDefault(edgeKey, 0) + 1;
        edgeWeights.put(edgeKey, weight);
        DefaultWeightedEdge edge = graph.getEdge(source, target);
        graph.setEdgeWeight(edge, weight);
        edgeColors.put(edge, defaultColor);
      } else {
        DefaultWeightedEdge edge = graph.addEdge(source, target);
        edgeWeights.put(edgeKey, 1);
        edgeColors.put(edge, defaultColor);
      }
    }
  }

  /**
   * Shows the directed graph and saves it as an image.
   *
   * @param graphFilePath the file path to save the graph image.
   */
  public void showDirectedGraph(String graphFilePath) {
    JGraphXAdapter<String, DefaultWeightedEdge> graphAdapter = new JGraphXAdapter<>(graph);
    mxIGraphLayout layout = new mxOrganicLayout(graphAdapter);
    layout.execute(graphAdapter.getDefaultParent());

    for (DefaultWeightedEdge edge : graph.edgeSet()) {
      String color = edgeColors.get(edge);
      String style =

          mxConstants.STYLE_STROKECOLOR + "=" + color + ";" + mxConstants.STYLE_FONTCOLOR + "=" +
              color + ";";
      graphAdapter.getEdgeToCellMap().get(edge).setStyle(style);

      double weight = graph.getEdgeWeight(edge);
      String label = String.format("%.2f", weight);
      graphAdapter.getEdgeToCellMap().get(edge).setValue(label);
    }

    BufferedImage image =
        mxCellRenderer.createBufferedImage(graphAdapter, null, 2, Color.WHITE, true, null);

    File imgFile = new File(graphFilePath);
    try {
      ImageIO.write(image, "PNG", imgFile);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * Prints all information about the WordGraph.
   */
  public void printGraphInfo() {
    System.out.println("Vertices:");
    for (String vertex : graph.vertexSet()) {
      System.out.println(" - " + vertex);
    }

    System.out.println("\nEdges:");
    for (DefaultWeightedEdge edge : graph.edgeSet()) {
      String source = graph.getEdgeSource(edge);
      String target = graph.getEdgeTarget(edge);
      double weight = graph.getEdgeWeight(edge);
      System.out.println(" - " + source + " -> " + target + " (Weight: " + weight + ")");
    }
  }

  /**
   * Queries the bridge words between two words.
   *
   * @param word1 the first word.
   * @param word2 the second word.
   * @return the list of bridge words.
   */
  public List<String> queryBridgeWords(String word1, String word2) {
    List<String> bridgeWords = new ArrayList<>();
    if (!graph.containsVertex(word1)) {
      System.out.println("No word1 in the graph!");
      return bridgeWords;
    }
    if (!graph.containsVertex(word2)) {
      System.out.println("No word2 in the graph!");
      return bridgeWords;
    }
    boolean bridgeFound = false;
    for (DefaultWeightedEdge edge : graph.edgeSet()) {
      String source = graph.getEdgeSource(edge);
      String target = graph.getEdgeTarget(edge);
      if (source.equalsIgnoreCase(word1)) {
        if (graph.containsEdge(target, word2)) {
          bridgeWords.add(target);
          bridgeFound = true;
        }
      }
    }
    if (!bridgeFound) {
      System.out.println("No bridge words from " + word1 + " to " + word2 + "!");
    } else {
      System.out.print("The bridge words from " + word1 + " to " + word2 + " are: ");
      for (int i = 0; i < bridgeWords.size(); i++) {
        if (i > 0) {
          System.out.print(", ");
        }
        System.out.print(bridgeWords.get(i));
      }
      System.out.println(".");
    }
    return bridgeWords;
  }

  /**
   * Calculates the shortest paths from a given word to all other words in the graph.
   *
   * @param word the starting word.
   */
  public void calcShortestPath(String word) {
    if (!graph.containsVertex(word)) {
      System.out.println("No " + word + " in the graph!");
      return;
    }
    String timestamp = String.valueOf(System.currentTimeMillis());

    for (String otherWord : graph.vertexSet()) {
      if (!otherWord.equals(word)) {
        calcShortestPath(word, otherWord, "one_to_other/" + timestamp);
      }
    }
  }

  /**
   * Calculates the shortest path between two words and saves the graph.
   *
   * @param word1      the starting word.
   * @param word2      the ending word.
   * @param folderName the folder to save the graph image.
   */
  public void calcShortestPath(String word1, String word2, String folderName) {
    boolean word1Exists = graph.containsVertex(word1);
    boolean word2Exists = graph.containsVertex(word2);

    if (!word1Exists || !word2Exists) {
      if (!word1Exists && !word2Exists) {
        System.out.println("Neither " + word1 + " nor " + word2 + " are in the graph!");
      } else if (!word1Exists) {
        System.out.println(word1 + " is not in the graph!");
      } else {
        System.out.println(word2 + " is not in the graph!");
      }
      return;
    }

    DijkstraShortestPath<String, DefaultWeightedEdge> shortestPathAlg =
        new DijkstraShortestPath<>(graph);
    GraphPath<String, DefaultWeightedEdge> shortestPath = shortestPathAlg.getPath(word1, word2);

    if (shortestPath == null) {
      System.out.println("No path found between " + word1 + " and " + word2);
      return;
    }

    List<DefaultWeightedEdge> shortestPathEdges = shortestPath.getEdgeList();
    double shortestPathLength = shortestPath.getWeight();

    List<List<DefaultWeightedEdge>> allShortestPaths = new ArrayList<>();
    List<DefaultWeightedEdge> currentPath = new ArrayList<>();
    findAllShortestPaths(word1, word2, currentPath, allShortestPaths, shortestPathLength);

    for (DefaultWeightedEdge edge : graph.edgeSet()) {
      String color = "#000000";
      int pathCount = countPathsContainingEdge(allShortestPaths, edge);
      if (pathCount == 1) {
        color = "#FF0000";
      } else if (pathCount > 1) {
        color = "#800080";
      }
      edgeColors.put(edge, color);
    }

    String graphFilePath = generateFilePath(word1, word2, folderName);
    showDirectedGraph(graphFilePath);

    System.out.println("All shortest paths between " + word1 + " and " + word2 + ":");
    for (List<DefaultWeightedEdge> path : allShortestPaths) {
      double pathLength = calculatePathLength(path);
      System.out.println("Path:");
      for (DefaultWeightedEdge edge : path) {
        String source = graph.getEdgeSource(edge);
        String target = graph.getEdgeTarget(edge);
        System.out.println(" - " + source + " -> " + target);
      }
      System.out.println("Path length: " + pathLength);
    }

    for (DefaultWeightedEdge edge : graph.edgeSet()) {
      edgeColors.put(edge, "#000000");
    }
  }

  private void findAllShortestPaths(String currentWord, String targetWord,
                                    List<DefaultWeightedEdge> currentPath,
                                    List<List<DefaultWeightedEdge>> allShortestPaths,
                                    double shortestPathLength) {
    if (currentWord.equals(targetWord)) {
      double pathLength = calculatePathLength(currentPath);
      if (pathLength == shortestPathLength) {
        allShortestPaths.add(new ArrayList<>(currentPath));
      }
      return;
    }

    for (DefaultWeightedEdge edge : graph.outgoingEdgesOf(currentWord)) {
      String nextWord = graph.getEdgeTarget(edge);
      if (!currentPath.contains(edge)) {
        currentPath.add(edge);
        findAllShortestPaths(nextWord, targetWord, currentPath, allShortestPaths,
            shortestPathLength);
        currentPath.remove(edge);
      }
    }
  }

  private double calculatePathLength(List<DefaultWeightedEdge> path) {
    double length = 0;
    for (DefaultWeightedEdge edge : path) {
      length += graph.getEdgeWeight(edge);
    }
    return length;
  }

  private int countPathsContainingEdge(List<List<DefaultWeightedEdge>> allPaths,
                                       DefaultWeightedEdge edge) {
    int count = 0;
    for (List<DefaultWeightedEdge> path : allPaths) {
      if (path.contains(edge)) {
        count++;
      }
    }
    return count;
  }

  private String generateFilePath(String word1, String word2, String fileType) {
    String timestamp = String.valueOf(System.currentTimeMillis());
    String directoryPath = "src/main/java/org/example/img/" + fileType + "/" + timestamp;
    osUtils.creatDirPath(directoryPath);
    return directoryPath + '/' + word1 + "-" + word2 + "-ShortestPaths.png";
  }

  /**
   * Performs a random walk on the graph and logs the path.
   */
  public void randomWalk() throws IOException {
    if (graph.vertexSet().isEmpty()) {
      System.out.println("Graph is empty!");
      return;
    }
    String timestamp = String.valueOf(System.currentTimeMillis());
    Random random = new Random();
    List<String> visitedNodes = new ArrayList<>();
    List<DefaultWeightedEdge> visitedEdges = new ArrayList<>();
    String currentNode = getRandomVertex(random);
    System.out.println("Starting random walk from node: " + currentNode);
    String logFileDir = "src/main/java/org/example/log/random_walk/" + timestamp;
    osUtils.creatDirPath(logFileDir);
    String logFilePath = logFileDir + "/walkTrack.txt";
    FileWriter writer = new FileWriter(logFilePath);
    writer.write("Random walk path:\n");
    while (true) {
      visitedNodes.add(currentNode);
      writer.write("Visited node: " + currentNode + "\n");
      List<DefaultWeightedEdge> outgoingEdges =
          new ArrayList<>(graph.outgoingEdgesOf(currentNode));
      if (outgoingEdges.isEmpty()) {
        System.out.println("No outgoing edges from node " + currentNode + ". Random walk ended.");
        break;
      }
      DefaultWeightedEdge nextEdge = getRandomEdge(random, outgoingEdges);
      if (visitedEdges.contains(nextEdge)) {
        System.out.println(
            "Edge " + nextEdge + " has already been visited. Exiting random walk.");
        return;
      }
      visitedEdges.add(nextEdge);
      String nextNode = graph.getEdgeTarget(nextEdge);
      System.out.println("Next node: " + nextNode);
      edgeColors.put(nextEdge, "#00FF00");
      String randomDicPath = "src/main/java/org/example/img/random_walk/" + timestamp;
      osUtils.creatDirPath(randomDicPath);
      showDirectedGraph(randomDicPath + "/tempGraph.png");
      currentNode = nextNode;
      System.out.print("Continue random walk? (yes/no): ");
      Scanner scanner = new Scanner(System.in);
      String input = scanner.nextLine().toLowerCase();
      if (!input.equals("yes")) {
        System.out.println("Random walk stopped by user.");
        break;
      }
    }
    writer.close();
  }
//    for (DefaultWeightedEdge edge : graph.edgeSet()) {
//      edgeColors.put(edge, "#000000");
//    }

  private String getRandomVertex(Random random) {
    List<String> vertices = new ArrayList<>(graph.vertexSet());
    return vertices.get(random.nextInt(vertices.size()));
  }

  private DefaultWeightedEdge getRandomEdge(Random random, List<DefaultWeightedEdge> edges) {
    return edges.get(random.nextInt(edges.size()));
  }

  /**
   * Generates a new text by inserting bridge words between the words of the input text.
   *
   * @param inputText the input text.
   * @return the generated text.
   */
  public String generateNewText(String inputText) {
    StringBuilder newTextBuilder = new StringBuilder();
    String[] words = inputText.split("\\s+");
    Random random = new Random();

    newTextBuilder.append(words[0]).append(" ");

    for (int i = 0; i < words.length - 1; i++) {
      String word1 = words[i].toLowerCase();
      String word2 = words[i + 1].toLowerCase();

      if (!graph.containsVertex(word1) || !graph.containsVertex(word2)) {
        newTextBuilder.append(words[i + 1]).append(" ");
        continue;
      }

      List<String> bridgeWords = queryBridgeWords(word1, word2);

      if (!bridgeWords.isEmpty()) {
        String randomBridgeWord = bridgeWords.get(random.nextInt(bridgeWords.size()));
        newTextBuilder.append(randomBridgeWord).append(" ");
      }

      if (!bridgeWords.contains(word2)) {
        newTextBuilder.append(words[i + 1]).append(" ");
      }
    }

    String newText = newTextBuilder.toString().trim();
    System.out.println("Generated new text: " + newText);
    return newText;
  }

  /**
   * Generates new text by interacting with the user.
   *
   * @return the generated text.
   */
  public String generateNewText() {
    Scanner scanner = new Scanner(System.in);
    String inputText;
    do {
      System.out.print("Enter the input text: ");
      inputText = scanner.nextLine();
      String filledText = generateNewText(inputText);
      System.out.print("Do you want to continue (yes/no)? ");
    } while (scanner.nextLine().equalsIgnoreCase("yes"));
    return inputText;
  }
}
